/*
 * MODIFIED FROM CODE ORIGINALLY AVAILABLE AT:
 *
 * http://bethecoder.com/open-source/csv/opencsv-2.3/au/com/bytecode/opencsv/CSVParser.java.html
 *
 * AND AVAILABLE UNDER THE FOLLOWING LICENSE:
 *
 * Copyright 2005 Bytecode Pty Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.metsci.glimpse.charts.vector.parser;

import java.io.IOException;
import java.io.LineNumberReader;
import java.io.Reader;
import java.text.ParseException;
import java.util.List;

/**
 * The tokenizer is an internal mechanism to the csv parser
 *
 * @author Kasper B. Graversen
 */
public class CsvTokenizer {

    enum PARSERSTATE {
        NORMAL, // normal text
        QUOTESCOPE, // inside quote scope (e.g.: " here "
    }
    LineNumberReader lnr;
    StringBuilder sb = null;

    public CsvTokenizer(final Reader stream) {
        lnr = new LineNumberReader(stream);
        sb = new StringBuilder(500);
    }

    private static void addSpaces(final StringBuilder sb, final int spaces) {
        for (int i = 0; i < spaces; i++) {
            sb.append(" ");
        }
    }

    /**
     * {@inheritDoc}
     */
    public void close() throws IOException {
        lnr.close();
    }

    /**
     * {@inheritDoc}
     */
    public int getLineNumber() {
        return lnr.getLineNumber();
    }

    /**
     * {@inheritDoc}
     */
    public boolean readStringList(final List<String> result) throws IOException, ParseException {
        result.clear();
        PARSERSTATE state = PARSERSTATE.NORMAL; // start out in normal mode

        // fast access to preferences
        final int quote = '"';
        final int delim = ',';

        String line;
        // read non-empty lines only
        do {
            line = lnr.readLine();
            if (line == null) {
                return false; // EOF
            }
        } while (line.length() == 0); // skip zero len lines

        // start parsing
        line += "\n"; // add a newline to determine end of line (making
        // parsing easier)

        sb.delete(0, sb.length()); // reset the stringbuilder

        // proccess the line (and maybe more lines of the file)
        int p = 0; // the pos of the cursor on the line
        int linenoQuoteState = -1; // the line number of the file where a potential multiline cell starts

        int potentialSpaces = 0; // spaces between words or after the last word.
        // when in non-quote mode, count the spaces and add them only if a non-delimiter or non-end-of-line is met
        // (otherwise its just empty stuff at the end of the cell which is ignored such as 'foo ,' which is read as
        // 'foo' whereas 'foo a' -> 'foo a'

        while (true) {
            // relies on p being incremented at least at the end of the while
            final char c = line.charAt(p);

            // react to char c depending on the state we are in
            switch (state) {

                case NORMAL:
                    // if(log.isDebugEnabled()) log.debug("normal " + p);

                    if (c == delim) {
                        result.add(sb.toString()); // save token
                        sb.delete(0, sb.length()); // reset the stringbuilder
                        potentialSpaces = 0;
                        break; // read more
                    } else if (c == ' ') { // trim starting spaces (trailing spaces
                        // are removed using the String.trim()
                        if (sb.length() > 0) {
                            // first on the line
                            potentialSpaces++;
                        }
                        break; // read more
                    } else if (c == '\n') {
                        // save token
                        result.add(sb.toString());
                        // done at start of method: sb.delete(0, sb.length()); // reset the stringbuilder
                        // done at start of method: potentialSpaces = 0;
                        return true; // we've read a line
                    } else if (c == quote) {
                        if (sb.length() == 0) { // quote first on line cannot be escaped
                            state = PARSERSTATE.QUOTESCOPE;
                            // update variable in order to do debug statements
                            linenoQuoteState = getLineNumber();
                            break; // read more
                        } else if (line.charAt(p + 1) == quote && sb.length() > 0) {
                            // an escaped quote - can not happen as first character, hence the "sb.length > 0"
                            addSpaces(sb, potentialSpaces);
                            potentialSpaces = 0;
                            sb.append(c); // add and skip the first quote
                            // (end of switch will skip the next quote)
                            p++;
                            break; // read more
                        } else if (line.charAt(p + 1) != quote) { // a single quote, change state and don't
                            // append
                            state = PARSERSTATE.QUOTESCOPE;
                            // update variable in order to do debug statements
                            linenoQuoteState = getLineNumber();
                            addSpaces(sb, potentialSpaces);
                            potentialSpaces = 0;
                            break; // read more
                        }
                    } else { // if just a normal character
                        addSpaces(sb, potentialSpaces);
                        potentialSpaces = 0;
                        sb.append(c); // add the char
                    }
                    break;

                // for each situation above, repeat now in the quote scope
                case QUOTESCOPE:
                    // System.out.println("quote: '" + p + "'");

                    if (c == '\n') { // newline does not count as newline in
                        // quote scope
                        sb.append('\n');
                        // parse the next line of the file
                        p = -1; // reset delta to point to start of new line (set to
                        // -1 as it will be incremented to 0 at the end of
                        // the switch)
                        line = lnr.readLine();
                        if (line == null) {
                            throw new ParseException(
                                    "File ended unexpectedly while reading a quoted cell starting on line ", linenoQuoteState);
                        }
                        line += '\n'; // add \n to make parsing easy
                        break; // read more
                    } else if (c == quote) {
                        if (line.charAt(p + 1) == quote) {
                            // an escaped quote,
                            sb.append(c); // add and skip the first quote (end of
                            // switch will skip the next quote
                            p++;
                            break; // read more
                        } else { // if(line.charAt(p + 1) != quote) {
                            // a single quote, only change state
                            state = PARSERSTATE.NORMAL;
                            break; // read more
                        }
                    } else { // if just a normal character or delimiter (they don't count in this mode)
                        sb.append(c); // add the char
                        // System.out.println("Adding char '" + c + "'");
                    }
                    break;
                default:
                    throw new RuntimeException("this can never happen!");

            } // switch

            p++; // read next char of the line
        }
    }
}
